package com.haoqizhe.kernel.log.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.haoqizhe.kernel.commons.constants.HeaderConstants;
import com.haoqizhe.kernel.commons.util.IpUtil;
import com.haoqizhe.kernel.core.entity.IUser;
import com.haoqizhe.kernel.log.annotation.NoOperateLog;
import com.haoqizhe.kernel.log.dto.OperateLog;
import com.haoqizhe.kernel.log.enums.LogTypeCodeEnum;
import com.haoqizhe.kernel.log.handler.OperateLogHandler;
import com.haoqizhe.kernel.log.helper.ThreadLocalHelper;
import com.haoqizhe.kernel.log.helper.UserAccountHelper;
import com.haoqizhe.kernel.log.properties.OperateLogProperties;
import com.haoqizhe.kernel.web.handler.GlobalExceptionHandler;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * Controller切面  统一日志采集
 *
 * @author 王梅
 * @date 2018/7/16 17:15
 */
@Aspect
@Component
@Slf4j
@EnableConfigurationProperties(OperateLogProperties.class)
@ConditionalOnProperty(prefix = "aspect.operate.log", name = "enabled", havingValue = "enabled", matchIfMissing = false)
public class OperateLogAspect {

    @Autowired
    private OperateLogProperties properties;

    @Autowired
    private OperateLogHandler operateLogHandler;

    /**
     * 系统名称
     */
    @Value("${spring.application.name:#{'unknown'}}")
    private String platformCode;

    /**
     * 切点定义
     */
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController) || " +
            "@annotation(org.springframework.web.bind.annotation.RestController) ||" +
            "@within(org.springframework.stereotype.Controller) ||" +
            " @annotation(org.springframework.stereotype.Controller)")
    private void operateLog() {

    }

    /**
     * 环绕通知
     *
     * @param point 连接点
     * @return object 实际执行的接口返回的结果
     * @throws Throwable 实际执行接口方法过程中抛的异常
     **/
    @Around(value = "operateLog()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //获取http请求接口url
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        // 从请求会话中获取当前用户信息
        UserAccountHelper helper = UserAccountHelper.getInstance();
        String userAccount = helper.getUserAccount();

        //将用户信息保存至  当前请求线程缓存
        Map<String, String> map = new HashMap<>(0);
        map.put("userAccount", userAccount);
        map.put("applicationName", platformCode);
        ThreadLocalHelper.setMap(map);

        //判断该接口是否需要记录日志
        boolean logFlag = this.isNeedToLog(point, httpServletRequest);
        //如果不需要，直接执行并返回结果
        if (!logFlag) {
            Object result = point.proceed();
            //请求执行完毕，从当前请求线程缓存移除用户信息
            ThreadLocalHelper.remove();
            return result;
        }

        //获得当前时间
        long startTime = System.currentTimeMillis();
        httpServletRequest.setAttribute("startTime", startTime);

        //执行实际的接口方法并获取执行结果
        Object result = point.proceed();

        //计算方法执行耗时
        long expandTime = System.currentTimeMillis() - startTime;
        try {
            //根据切面参数封装操作日志记录
            OperateLog operateLog = createOperateLog(point, httpServletRequest, "1", userAccount);
            //耗时
            operateLog.setExpendTime(expandTime);
            //设置返回结果
            operateLog.setApiResponse(this.deleteSensitiveContent(result));
            // 发送日志
            sendLog(operateLog);
        } catch (Exception ex) {
            //方法名
            String methodName = this.getMethodName(point);
            log.error("系统操作成功日志采集出错，请求接口：" + methodName, ex);
        }
        //请求执行完毕，从当前请求线程缓存移除用户信息
        ThreadLocalHelper.remove();
        //返回接口执行结果
        return result;
    }

    /**
     * 异常通知
     *
     * @param point 连接点
     * @param ex    实际执行的接口方法抛出的异常
     */
    @AfterThrowing(value = "operateLog()", throwing = "ex")
    public void afterThrowing(JoinPoint point, Throwable ex) {
        //获取http请求接口url
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Long startTime = (Long) httpServletRequest.getAttribute("startTime");
        Long expandTime = null;
        if (startTime != null) {
            expandTime = System.currentTimeMillis() - startTime;
        }
        //判断该接口是否需要记录日志
        boolean logFlag = this.isNeedToLog(point, httpServletRequest);
        //如果不需要，直接执行并返回结果
        if (!logFlag) {
            //请求执行完毕，从当前请求线程缓存移除用户信息
            ThreadLocalHelper.remove();
            return;
        }
        try {
            //从当前请求线程缓存中获取用户信息
            Map<String, String> map = ThreadLocalHelper.get();
            String userAccount = map == null ? "unknown" : map.get("userAccount");
            //用完用户信息后 从当前请求线程缓存移除用户信息
            ThreadLocalHelper.remove();
            //根据切面参数封装操作日志记录
            OperateLog operateLog = createOperateLog(point, httpServletRequest, "0", userAccount);
            operateLog.setExpendTime(expandTime);
            //设置返回结果  记录异常堆栈信息
            String strTrace = "";
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] traceElementList = ex.getStackTrace();
            for (StackTraceElement traceElement : traceElementList) {
                sb.append(traceElement).append("\r\n ");
            }
            if (sb.length() > 0) {
                strTrace = sb.toString();
            }
            operateLog.setApiResponse(strTrace);
            // 发送日志
            sendLog(operateLog);
        } catch (Exception innerEx) {
            //方法名
            String methodName = this.getMethodName(point);
            log.error("系统操作失败日志采集出错，请求接口：" + methodName, innerEx);
        }
    }

    /**
     * 判断被请求的接口是否需要记录日志，从方法签名 和 url 两方面判断
     *
     * @param point   连接点
     * @param request http请求
     * @return
     */
    private boolean isNeedToLog(JoinPoint point, HttpServletRequest request) {
        String requestUrl = request.getRequestURI();
        //判断该url是否需要记录日志
        boolean urlLogFlag = this.needToLogUrl(requestUrl);
        //如果不需要，直接执行并返回结果
        if (!urlLogFlag) {
            return false;
        }

        //获取实际的接口方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        //判断方法是否需要记录日志
        boolean methodLogFlag = this.needToLogMethod(method);
        //如果不需要，直接执行并返回结果
        if (!methodLogFlag) {
            return false;
        }

        NoOperateLog noOperateLog = method.getAnnotation(NoOperateLog.class);
        if (noOperateLog != null) {
            return false;
        }

        return true;
    }

    /**
     * 根据切面参数  获取操作日志相关信息，根据这些信息创建并返回一个操作日志实体
     *
     * @param point       连接点
     * @param request     http请求
     * @param successInd  成功标志
     * @param userAccount 当前用户账号
     * @return SysOperateLog 封装了请求成功或失败的信息  的操作日志实体
     */
    private OperateLog createOperateLog(JoinPoint point, HttpServletRequest request, String successInd, String userAccount) {
        //获取方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String methodName = this.getMethodName(point);
        //封装操作日志实体
        OperateLog operateLog = new OperateLog();
        //格式化请求参数
        String params = this.getParamsJson(point);

        //设置当前用户信息
        operateLog.setPlatformCode(platformCode);
        operateLog.setUserAccount(userAccount);
        //日志类型
        handlerLogTypeCode(request, methodName, operateLog);
        //日志内容
        String strSuccess = "1".equals(successInd) ? "成功" : "失败";
        String content = "请求" + methodName + "接口" + strSuccess;
        operateLog.setLogContent(content);
        //请求ip信息
        operateLog.setRequestSource(request.getHeader(HeaderConstants.CALL_SOURCE));
        operateLog.setRequestIp(IpUtil.getRealIp(request));
        operateLog.setServerIp(IpUtil.getServiceIp());
        operateLog.setRequestParam(params);
        //接口信息
        operateLog.setApiUrl(request.getRequestURI());
        operateLog.setApiName(methodName);
        ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
        if (apiOperation != null) {
            String apiName = apiOperation.value().length() == 0 ? methodName : apiOperation.value();
            operateLog.setApiName(apiName);
            operateLog.setApiDesc(apiOperation.notes());
        }
        //成功标志
        operateLog.setSuccessInd(successInd);
        //设置创建时间
        operateLog.setCreateTime(System.currentTimeMillis() / 1000);
        return operateLog;
    }

    /**
     * 日志类型
     *
     * @param request    HttpServletRequest
     * @param methodName methodName
     * @param operateLog operateLog
     */
    private void handlerLogTypeCode(HttpServletRequest request, String methodName, OperateLog operateLog) {
        String lastName = methodName.substring(methodName.lastIndexOf(".") + 1);
        operateLog.setLogTypeCode(LogTypeCodeEnum.findCode(lastName));
        if ("other".equals(operateLog.getLogTypeCode())) {
            switch (request.getMethod()) {
                case "GET":
                    operateLog.setLogTypeCode(LogTypeCodeEnum.query.getValue());
                    break;
                case "POST":
                    operateLog.setLogTypeCode(LogTypeCodeEnum.save.getValue());
                    break;
                case "PUT":
                    operateLog.setLogTypeCode(LogTypeCodeEnum.update.getValue());
                    break;
                case "DELETE":
                    operateLog.setLogTypeCode(LogTypeCodeEnum.delete.getValue());
                    break;
                default:
            }
        }
    }

    /**
     * 获取接口方法名
     *
     * @param joinPoint 连接点
     * @return
     */
    private String getMethodName(JoinPoint joinPoint) {
        //获取完整方法名
        String methodName = joinPoint.getSignature().toShortString();
        //去掉方法名带的后缀
        String shortMethodNameSuffix = "(..)";
        if (methodName.endsWith(shortMethodNameSuffix)) {
            methodName = methodName.substring(0, methodName.length() - shortMethodNameSuffix.length());
        }
        return methodName;
    }

    /**
     * 判断url是否需要记录日志
     *
     * @param url 请求的url
     */
    private boolean needToLogUrl(String url) {
        //先获取配置文件中的  需要忽略的url列表
        List<String> ignoreUrlList = getIgnoreUrlList();
        //如果请求的url在需要忽略url列表里  则返回false，不需要记录日志
        for (String ignoreUrl : ignoreUrlList) {
            if (url.contains(ignoreUrl)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断方法是否需要记录日志
     *
     * @param method 实际执行的接口方法
     */
    private boolean needToLogMethod(Method method) {
        // 全局异常处理类中的方法 和私有方法  不需要记录日志
        return !method.getDeclaringClass().equals(GlobalExceptionHandler.class) && Modifier.isPublic(method.getModifiers());
    }

    /**
     * 从连接点中 获取json格式的请求参数,过滤Http相关自带参数
     *
     * @param joinPoint 连接点
     * @return
     */
    private String getParamsJson(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        StringBuilder sb = new StringBuilder();
        for (Object arg : args) {
            if (arg instanceof HttpServletResponse ||
                    arg instanceof HttpServletRequest ||
                    arg instanceof MultipartFile ||
                    arg instanceof HttpSession ||
                    arg instanceof IUser) {
                continue;
            }
            // 移除敏感内容
            String paramStr = this.deleteSensitiveContent(arg);
            sb.append(paramStr).append(",");
        }
        if (sb.length() == 0) {
            return "";
        }
        return sb.deleteCharAt(sb.length() - 1).toString();
    }

    /**
     * 移除参数中的敏感内容
     *
     * @param obj 参数对象
     * @return 用******代替敏感内容后的参数对象
     */
    private String deleteSensitiveContent(Object obj) {
        JSONObject jsonObject;
        List<Map> list;
        // 参数为空 则返回空json
        boolean ex = obj == null || obj instanceof Exception || (obj instanceof List && ((List) obj).size() == 0);
        if (ex) {
            jsonObject = new JSONObject();
            return jsonObject.toString();
        }

        try {
            //将参数格式化为json字符串
            String param = JSONUtil.toJsonStr(obj);
            //获取配置文件中的敏感字段
            List<String> sensitiveFieldList = this.getSensitiveFieldList();

            // 如果参数是数组 数组方式解析
            String arrStart = "[";
            String objectStart = "{";
            if (param.startsWith(arrStart)) {
                JSONArray jsonArray = JSONUtil.parseArray(param);
                list = JSONUtil.toList(jsonArray, Map.class);
                if (CollUtil.isNotEmpty(list)) {
                    //将包含敏感字段的参数的值 改为“******”
                    for (Map map : list) {
                        for (String sensitiveField : sensitiveFieldList) {
                            if (map.containsKey(sensitiveField)) {
                                map.put(sensitiveField, "******");
                            }
                        }
                    }
                    return JSONUtil.toJsonStr(list);
                }
            }
            // 参数是对象  对象方式解析
            else if (param.startsWith(objectStart)) {
                jsonObject = JSONUtil.parseObj(param);
                if (jsonObject != null) {
                    //将包含敏感字段的参数的值 改为“******”
                    for (String sensitiveField : sensitiveFieldList) {
                        if (jsonObject.containsKey(sensitiveField)) {
                            jsonObject.put(sensitiveField, "******");
                        }
                    }
                    return jsonObject.toString();
                }
            }
        } catch (ClassCastException e) {
            return String.valueOf(obj);
        }
        return String.valueOf(obj);
    }

    /**
     * 获取需要忽略的url列表（可配置）
     */
    private List<String> getIgnoreUrlList() {
        List<String> ignoreUrlList = new ArrayList<>();
        if (StrUtil.isNotBlank(properties.getIgnoreUrls())) {
            ignoreUrlList.addAll(Arrays.asList(properties.getIgnoreUrls().split(",")));
        }
        return ignoreUrlList;
    }

    /**
     * 获取敏感字段列表（可配置）
     */
    private List<String> getSensitiveFieldList() {
        List<String> sensitiveFieldList = new ArrayList<>();
        if (StrUtil.isNotBlank(properties.getSensitiveFields())) {
            sensitiveFieldList.addAll(Arrays.asList(properties.getSensitiveFields().split(",")));
        }
        return sensitiveFieldList;
    }

    /**
     * 将消息发送到上层应用
     *
     * @param operateLog 要发送的日志消息
     */
    private void sendLog(OperateLog operateLog) {
        if (operateLogHandler != null) {
            try {
                operateLogHandler.handle(operateLog);
            } catch (Exception e) {
                log.warn("OperateLogHandler接口处理日志时发生错误", e);
            }
        } else {
            log.warn("未实现OperateLogHandler接口，无法处理日志。");
        }
    }
}
